<?php
/**
 * This file is part of DomAr.
 * 
 * DomAr is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 * 
 * DomAr is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 * 
 * You should have received a copy of the GNU General Public License
 * along with DomAr.  If not, see <http://www.gnu.org/licenses/>.
 * 
 * @link http://inditel.ee/
 * @copyright 2007-2009 Inditel Meedia OÜ
 * @author Oliver Leisalu <oliver@inditel.ee>
 */

/**
* @package DomAr
*/ 
abstract class DomArObject implements ArrayAccess{

	/**
	* @var Int
	*/
	private $id = 0;
	/**
	* @var Array
	*/
	public $_changed = array();
	/**
	* @var Bool
	*/
	protected $serialized = false;
	
	
	
	/**
	* @var Bool
	*/
	protected $validated = true;
	/**
	* All failed validations will be put into this array.
	* @var Array
	*/
	protected $failedValidations = array();
	/**
	* @var Bool
	*/
	protected $validationEnabled = true;
	
	
	// ----------------------------------------
	// CONSTRUCT / DESTRUCT
	// ----------------------------------------
	
	/**
	* If you want do define your own constructor use method called "construct"
	*/
	final public function __construct() {
		$args = func_get_args();
		if( $args[0] == 'id' ) {
			// construct method should not be called here. ID will be passed only internally and construct will be called there manually.
			$this->setId( $args[1] );
		} else {
			call_user_func_array( array($this, 'construct'), $args );
			$this->init();
		}
	}
	
	/**
	* Constructor
	* 
	* Constructor can't have any arguments! 
	* If object is loaded from database object already knows its ID when this method is called.
	*/
	public function construct() {}
	
	/**
	 * Init new object
	 * 
	 * This method will only be called when new instance of object is created and it is not loaded from database.
	 * Basicly when you manually write $something = new SomeClass();
	 * This method will not be called on saved objects. (They have id)
	 */
	public function init() {}
	/**
	 * Init loaded object
	 * 
	 * This method will only be called when object is loaded from database. (Not called when new instance created.)
	 */
	public function initLoaded() {}
	
	
	// ----------------------------------------
	// SAVE
	// ----------------------------------------
	
	/**
	* Saves object into database
	*/
	final public function save() {
		$session = new DomArSession( $this );
		return $session->save();
	}
	
	/**
	* @internal
	*/
	public function _beforeInsert() {}
	/**
	* @internal
	*/
	public function _insert( DomArValueSet $set ) {}
	/**
	* @internal
	*/
	public function _afterInsert() {}
	/**
	* @internal
	*/
	public function _beforeUpdate() {}
	/**
	* @internal
	*/
	public function _update( array $changed, DomArValueSet $set ) {}
	/**
	* @internal
	*/
	public function _afterUpdate() {}
	
	/**
	* @internal
	*/
	abstract public static function _updateTableFields();
	
	// ----------------------------------------
	// LOAD
	// ----------------------------------------
	
	/**
	* Load object data from database.
	* @return DomSqlIterator
	*/
	abstract public static function loadData($id, $fields = '*');
	
	/**
	* Loads object from database. If not found returns NULL.
	* @param Int $id Object id
	* @return DomArObject
	*/
	abstract public static function load( $id );
	
	/**
	 * Makes id set query. This query returns id-s as keys and $field as value.
	 * @param String $field
	 * @return DomArDataQuery
	 */
	abstract public static function idSetQuery( $field = 'id' );
	/**
	* Sends custom query to database and returns data as array.
	* @param String $fields Fields that will be loaded
	* @return DomArDataQuery
	*/
	abstract public static function dataQuery();

	/**
	* Sends custom query to database and returns objects
	* @return DomArQuery
	*/
	abstract public static function query();
	
	/**
	* Creates object from data. This is meant for advance use only. 
	* If you load object data from database you can use this method to convert data to object. Do not use it to create new instances, 
	* use it only for creating objects that are saved in database.
	* @param Array
	*/
	abstract public static function create( array &$data );
	
	/**
	* @internal
	*/
	public function _assignData( array &$data ) {}
	
	
	
	

	// ----------------------------------------
	// DELETE
	// ----------------------------------------
	
	/**
	* Deletes object from database
	* @return Bool If object was deleted.
	*/
	abstract public function delete();
	
	/**
	* @internal
	*/
	protected function _beforeDelete() {}
	
	/**
	* @internal
	*/
	protected function _afterDelete() {}
	
	// ----------------------------------------
	// SERIALIZE
	// ----------------------------------------
	
	/**
	* Caches object into session. 
	*
	* Useful for caching manager objects that you need everytime.
	* This cache is used by ClassName::load(id) method which checks for cache and if not exist then load object from database.
	*/
	public function cache() {
		$_SESSION[DomAr::CACHED_OBJECTS_CACHE_KEY][get_class($this) . '_' .  $this->getId()] = $this->serialize();
	}
	
	/**
	* Serialize object. 
	* 
	* See PHP serialize method.
	* Do not use PHP function serialize on DomArObjects as it creates alot overhead. Use $item->serialize().
	*/
	public function serialize() {
		$this->_serializeData();
		return serialize($this);
	}
	
	/**
	* @internal
	*/
	public function _serializeData() {}
	
	/**
	* Unserializes string which containts serialized DomArObject
	* 
	* If DomArObject what is beeing unserialized already exists it will not be unserialized - it will return already existing object. 
	* So if you make changes to object and serialize it then be sure to unserialize it before loading it, otherwise changes will be lost!
	* @param String $var
	* @return DomArObject
	*/
	public static function unserialize( $var ) {
		
		$object = unserialize($var);
		
		if( is_object($object) ) {
			if( $object->getId() ) {
				$exists = DomAr::getObject( $object->getId(), get_class($object) );
				if( $exists ) {
					if( $exists->serialized === true )
						$exists->_unserializeData();
					return $exists;
				} else {
					DomAr::storeObject( $object );
				}
			}
			$object->_unserializeData();
			return $object;
		}
		return NULL;
	}
	
	/**
	* @internal
	*/
	public function _unserializeData() {}	
	
	// ----------------------------------------
	// GET
	// ----------------------------------------
	
	/**
	* @internal
	*/
	protected function get( $var ) {
		if( $var === 'id' )
			return $this->id;
		else
			throw new RuntimeException('Cannot access '.$var);
	}
	
	/**
	* Accesses object property without any checking if its loaded (See lazyloading) or anything else, just returns property current value.
	* @param String $var
	* @return Mixed
	*/
	protected function &access( $var ) {
		throw new RuntimeException('Cannot access '.$var);
	}
	
	/**
	* Returns object variable.
	* @param String $var
	* @return Mixed
	*/
	public function __get( $var ) { 
		if( method_exists($this, 'get'.ucfirst($var) ) )
			return $this->{'get'.ucfirst($var)}();
		else
			throw new RuntimeException('No get method for <strong>'.$var.'</strong>!');
	}
	
	/**
	* Returns object id.
	* @return Int
	*/
	final public function getId() {
		return $this->id;
	}
	
	/**
	* Returns all object property handlers.
	* @return Array
	*/
	public static function getAllProperties() {
		return array();
	}
	
	/**
	* Returns all object property handlers in single class. 
	*
	* If class A extends B and you call A::getProperties() you only get class A property handlers.
	* For getting all properties see DomArObject::getAllProperties()
	* @return Array
	*/
	public static function getProperties() {
		return array();
	}
	
	/**
	* Returns property with name $name property handler.
	* @param String $name
	* @return DomArProperty
	*/
	public static function getProperty( $name ) {
		return NULL;
	}
	
	/**
	* Returns list with property names that are concidered changed and will be saved when $item->save() method is called.
	* @return Array
	*/
	public function getChanged() {
		return $this->_changed;
	}
		
	/**
	* @return DomArTable
	*/
	abstract public static function getTable();
	
	/**
	* Returns table name where object data is held.
	* @return String
	*/
	abstract public static function getTableName();
	
	/**
	* Returns database name where objects table is.
	* @return String
	*/
	abstract public static function getDatabase();
	
	/**
	* Returns string with database and tablename like ´database´.´tableName´
	* Use this method to make custom queries.
	* @return String
	*/
	abstract public static function getDbTable();
	
	// ----------------------------------------
	// SET
	// ----------------------------------------
	
	/**
	 * Validate all unchanged properties. This method is used before inserting new object to database.
	 */
	public function validateUnchangedProperties() {
		
	}
	/**
	 * @param String $propertyName
	 * @param Mixed $value
	 */
	public function _beforeValidate( $propertyName, &$value ) {
		return;
	}
	/**
	 * @param String $propertyName
	 * @param Mixed $value
	 */
	public function _validate( $propertyName, &$value ) {
		return 0;
	}
	/**
	 * @param String $propertyName
	 * @param Mixed $value
	 */
	public function _beforeSet( $propertyName, &$value ) {
		return;
	}
	/**
	 * @param String $propertyName
	 */
	public function _afterSet( $propertyName ) {
		return;
	}
	/**
	* Sets property to value to new value.
	*
	* $config variable can be used to ignore some steps.
	* <ul>
	* <li>ignoreSetPropertyListener - ignores property set listener which is defined by IDomArPropertySetListener interface.</li>
	* </ul>
	* 
	* @internal
	* @param String $var
	* @param Mixed $value
	* @param Array $config
	*/
	protected function set( $var, $value, array $config = array() ) {
		if( $var === 'id' )
			$this->setId($value);
		else
			throw new RuntimeException('Cannot access '.$var);
	}
	
	/**
	* Sets object property value. 
	*
	* No validation nor checking listeners is done, just setting property value to that value. 
	* Be careful when using this! Meant for internal use.
	* @param String $var property name
	* @param Mixed $value new property value
	*/
	public function forceSet( $var, $value ) {
		throw new RuntimeException('Cannot access '.$var);
	}
	
	/**
	* @param String $var
	* @param Mixed $value
	*/
	public function __set( $var, $value ) { 
		$this->{'set'.ucfirst($var)}($value);
	}
	
	/**
	* Sets object id. Setting id is not allowed when object already has id!
	* Object will be also stored in storedObjectsList.
	* @param Int $id
	*/
	final public function setId( $id ) {
		if( $this->id === 0 or $id === 0 ) {
			
			// object ID is being reseted, remove it from store.
			if( $this->id > 0 && $id == 0 )
				DomAr::unStoreObject( $this );
			
			$this->id = (int)$id;
			
			// Object obtained id, add it to store.
			if( $this->id > 0 )
				DomAr::storeObject( $this );
				
		} else {
			throw new RuntimeException('Can\'t overwrite id', 100 );
		}
	}
	
	/**
	* Sets proeprty to changed.
	* @param String Property name
	*/
	final public function changed( $property ) {
		$this->_changed[$property] = true;
	}
	
	/**
	* Sets property changed status to new value.
	* @param $property Property name
	* @param Boolean $value true if changed, false if not changed.
	*/
	final public function setChanged( $property = NULL, $value = NULL ) {
		if( $property === false ) {
			$this->_changed = array();
		} else {
			if( $value === true )
				$this->changed( $property );
			else 
				unset($this->_changed[$property]);
		}
	}
	
	/**
	* @param String $property Leave it to NULL if you want to know if any of the record properties is changed. Add property name if you want to know if that property is changed.
	* @return Boolean If object record property(es) is changed
	*/
	final public function isChanged( $property = NULL ) {
		if( $property === NULL )
			return !empty($this->_changed);
		else
			return (bool)$this->_changed[$property];
	}
	
	// ----------------------------------------
	// ARRAY ACCESS
	// ----------------------------------------
	
	/**
	 * @param Mixed $propertyName
	 * @return Mixed
	 */
	public function offsetGet( $propertyName ) {
		return $this->$propertyName;
	}
	/**
	 * @param Mixed $propertyName
	 * @param Mixed $value
	 */
	public function offsetSet( $propertyName, $value) {
		$this->$propertyName = $value;
	}
	/**
	 * @param Mixed $propertyName
	 * @return Bool
	 */
	public function offsetExists( $propertyName ) {
		return (bool)$this->getProperty( $propertyName );
	}
	/**
	 * Throws exception. This is not supported.
	 * @param unknown_type $propertyName
	 */
	public function offsetUnset( $propertyName ) {
		throw new RuntimeException('Unsetting DomArObject properties is not supported. Set value to NULL instead.');
	}
	
	// ----------------------------------------
	// CLONE
	// ----------------------------------------
	
	/**
	* @internal
	*/
	final public function __clone() {
		$this->_clone();
	}
	
	/**
	* @internal
	*/
	protected function _clone() {}
	
	// ----------------------------------------
	// MISC
	// ----------------------------------------

	/**
	* @internal
	*/
	public static function buildMetadata() {}

	// ----------------------------------------
	// VALIDATION
	// ----------------------------------------
	
	/**
	* Defines custom validation errors.
	* Format is like array(propertyName=>array(ErrorCode => Message))
	* In message you can use keyword [$value] to get value that failed validation into your message.
	*
	* You can also create validationErrors.lang.php file into your conf folder which containts all validation errors. 
	* Sample file contents
	* <code>
	* $DOMAR_VALIDATION_ERRORS = array(
	*	'CLASS_NAME' => array(
	*		'PROPERTY_NAME' => array(
	*			ERROR_NR  => 'ERROR_MESSAGE'
	*		),
	*	),
	* );
	* </code>
	* @return Array
	*/
	public function getValidationErrorMessages() {
		return array();
	}
	
	/**
	 * @param String $propertyName Add only if you want to know if specific property validated.
	* @return Bool true if object is validated
	*/
	final public function validated( $propertyName = NULL ) {
		if( $propertyName === NULL )
			return $this->validated;
		else {
			foreach( $this->failedValidations as $validation ) {
				if( $validation['property'] == $propertyName )
					return false;
			}
			return true;
		}
	}
	
	/**
	* Change object validated status
	* @param Bool $value
	*/
	final public function setValidated() {
		$this->validated = true;
		$this->failedValidations = array();
	}
	
	/**
	* @return DomArValidationReport
	*/
	final public function getValidationReport() {
		return DomArValidationReport::getLastReport();
	}
	
	/**
	* Returns last validation error message (User message if defined)
	* @return String
	*/
	final public function getLastValidationMessage() {
		return $this->failedValidations[0]['userMessage'];
	}
	
	/**
	* Returns array where values are user error messages. (If user error message is found, otherwise system error message will be used!)
	* @return Array
	*/
	final public function getValidationMessages( $separator = false ) {
		$list = array();
		foreach( $this->failedValidations as $validation )
			$list[] = $validation['userMessage'];
			
		if( $separator !== false )
			$list = implode($separator, $list);

		return $list;
	}
	
	/**
	* Returns array with information about all failed validations.
	* @return Array 
	*/
	final public function getValidationInfo() {
		return $this->failedValidations;
	}
	
	/**
	* Returns array with values
	* <ul>
	* <li>code - errorCode</li>
	* <li>property - property that new value caused validation error</li>
	* <li>value - value that failed validation</li>
	* <li>message - error message. If defined uses user validation error messages )</li>
	* <li>sysMessage - error message This do not use user validation error messages, it is always system error message.</li>
	* <li>solved - Boolean if validate() method is called with argument true to clear last validation error.)</li>
	* <li>object - Object that caused error </object>
	* </ul>
	* @return Array 
	*/
	final public function getLastValidationInfo() {
		return $this->failedValidations[0];
	}
	
	/**
	* Disable/Enable validation
	* @param Bool $value 
	*/
	final protected function validationEnabled( $value ) {
		$this->validationEnabled = (bool)$value;
		$this->validated = true;
	}
	
	/**
	 * @param Int $errorNr error nr
	 * @param String $var property name
	 * @param Mixed $value Value that failed validation
	 * @param Array $messageValues add custom values that will be replaced in final message. Use key value pairs where key is identification.
	* @internal
	*/
	final public function registerFailedValidation( $errorNr, $var, $value, array $messageValues = array() ) {
		if( $this->validationEnabled === true ) {
			$this->validated = false;
			array_unshift( $this->failedValidations, 
				array(
					'errorNr' => $errorNr,
					'property' => $var,
					'value' => $value,
					'userMessage'=> DomArValidationReport::makeValidationMessage( $this, $errorNr, $var, $value, true, $messageValues),
					'errorMessage'=> DomArValidationReport::makeValidationMessage( $this, $errorNr, $var, $value, false, $messageValues),
					'object' => $this,
				)
			);
		}
	}
	
	/**
	* @internal
	*/
	final protected function analyzeAccess( $propertyName ) {
		$back = debug_backtrace();

		foreach( $back as $i =>  $keys ) {
			if( is_object($back[$i-1]['object']) && get_class($back[$i-1]['object']) == get_class($this) && $keys['function'] == '__get' or $keys['function'] == '__set' ) {
				$key = $i+1;
				break;
			}
		}

		if( $back[$key]['class'] != get_class($this) ) {
			throw new RuntimeException('
				Cannot access <strong>private or protected</strong> property <strong>
				'.get_class($this).'::'.$propertyName.'
				</strong> in <strong>'.$back[$key-1]['file'].'
				</strong> on line <strong>'.(string)$back[$key-1]['line'].'</strong>'
			);
		}
	}
	
}
?>